跳到主要内容

Vue3学习

参考资料

首先当然是 官方的 Vue3.0 文档 参考资料 Vue3 中文文档 参考资料 彻底理解服务端渲染 - SSR原理 参考资料 快速使用Vue3最新的15个常用API (这个总结的很棒!)

使用 Vite 构建项目

参考资料 Vite 官方库

# 安装 Vite
npm install -g create-vite-app

npm init vite-app <project-name>
cd <project-name>
npm install
npm run dev

接下来直接访问 3000 端口

Diff 算法和静态提升

Difference 算法不同,在 Vue2 时是全量比较(一些不会改变的静态 DOM 也需要比较),而到了 Vue3 是给变化的 DOM 打上一个 patch flag 来标识变化,这样那些静态的 DOM 就无需比较了。

hoistStatic 静态提升

  • Vue2 中无论元素是否参与更新,每次都会重新创建,然后再渲染
  • Vue3 中对于不参与更新的元素,都会静态提升,只会被创建一次,在渲染时直接复用

例如下面转换同一个模板,检查输出内容

<div>
<div>hello vue</div>
<div>hello vue</div>
<div>hello vue</div>
<div>{{ msg }}</div>
</div>

Vue2 的 模板转换工具

// Vue2.x
function render() {
with(this) {
return _c('div', [_c('div', [_v("hello vue")]), _c('div', [_v("hello vue")]),_c('div', [_v("hello vue")]), _c('div', [_v(_s(msg))])
])
}
}

Vue3 的 模板转换工具

// Vue3.x
import { createVNode as _createVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createBlock as _createBlock } from "vue"

// 可以看到这部分不变的静态 DOM 被抽离了出来
const _hoisted_1 = /*#__PURE__*/_createVNode("div", null, "hello vue", -1 /* HOISTED */)
const _hoisted_2 = /*#__PURE__*/_createVNode("div", null, "hello vue", -1 /* HOISTED */)
const _hoisted_3 = /*#__PURE__*/_createVNode("div", null, "hello vue", -1 /* HOISTED */)

export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock("div", null, [
_hoisted_1,
_hoisted_2,
_hoisted_3,
_createVNode("div", null, _toDisplayString(_ctx.msg), 1 /* TEXT */)
]))
}

// Check the console for the AST

可以看到 Vue3 在变化的部分绑定数据且加上了一个为 1 的 patch flag 标识其为 TEXT,且把不变的部分抽离出来并标识其为 -1(静态提升)

事件侦听缓存

CacheHandlers 事件侦听器缓存,默认情况下 onClick 会被视为动态绑定,所以每次都会去追踪它的变化,但是因为是同一个函数,所以没有追踪变化,直接缓存起来复用即可

<div>
<button @click="onClick">按钮</button>
</div>

Vue3 的 模板转换工具

设置 CacheHandlers 之前,可以看到 Vue3.x 还是给其添加了个 8 的静态标记,标明其需要进行追踪

import { createVNode as _createVNode, openBlock as _openBlock, createBlock as _createBlock } from "vue"

export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock("div", null, [
_createVNode("button", { onClick: _ctx.onClick }, "按钮", 8 /* PROPS */, ["onClick"])
]))
}

// Check the console for the AST

设置 CacheHandlers 之后,没有这个静态标记了

import { createVNode as _createVNode, openBlock as _openBlock, createBlock as _createBlock } from "vue"

export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock("div", null, [
_createVNode("button", {
onClick: _cache[1] || (_cache[1] = (...args) => (_ctx.onClick(...args)))
}, "按钮")
]))
}

// Check the console for the AST

Vue2 与 Vue3 代码组织区别

参考资料 简明扼要聊聊 Vue3.0 的 Composition API 是啥东东!

在 Vue2 时的代码组织方式( Options API):

export default {
props: {
……
},
data() {
return {
……
};
},
watch: {
……
},
computed: {
……
}
methods: {
……
}
}

当一个组件里面内容多的化这种代码组织方式就会变的十分臃肿,所以在 Vue3 中引入了 Composition API 这种组织方式

// 例如编写了一个鼠标监听模块 listenMouse.js
import { ref, onMounted, onUnmounted } from 'vue'
function useMouse() {
const x = ref(0)
const y = ref(0)
const update = e => {
x.value = e.pageX
y.value = e.pageY
}
onMounted(() => {
window.addEventListener('mousemove', update)
})
onUnmounted(() => {
window.removeEventListener('mousemove', update)
})
return { x, y }
}
export default useMouse;

而在组件里就可以直接引用上面的模块使用

<template>
<p>Mouse Position:</p>
<p>x:{{ x }},y:{{ y }}</p>
</template>
<script>
import useMouse from './listenMouse';
export default {
setup (props) {
let {x, y} = useMouse();
return {
x,
y
}
}
}
</script>

通过对 Composition API 的了解,你会发现在代码组织上有一个很大的变化:干掉了 Vue2.x 中神奇的 this

在 Vue2.x 中,我们的代码中大量的使用到了 this,组件中的 props, data,methods 都是绑定到 this 上下文,然后由 this 去访问。

那么使用 Composition API 之后,当涉及到跨组件之间提取、复用逻辑时,就会非常的灵活。一个合成函数只依赖于它的参数和全局引入的 Vue APIs,而不是充满魔法的 this 上下文。我们只需要将组件中你想复用的那部分代码抽离,然后将它导出为函数就可以了。 比如上面中的 listenMouse.js,单独导出 useMouse,在其他组件都可以使用。

但是这也引出了函数式编程的问题:面向过程通过划分功能模块,通过函数相互间的调用来实现,但是需求变化时就需要更改函数。而你改动的函数有多少的地方在调用它,关联多少数据,这是很不容易弄清楚的地方

回过头来看 Options API 的约定:

  • props 里面设置接收参数
  • data 里面设置变量
  • computed 里面设置计算属性
  • watch 里面设置监听属性
  • methods 里面设置事件方法

Options APi 约定了我们该在哪个位置做什么事,强制对当前代码进行分割,这样虽然不太灵活但是至少结构上是挺直观的。

Composition API

参考资料 官方文档 什么是组合式 API? 参考资料 VUE 3.0 学习探索入门系列 - Vue3.x 生命周期 和 Composition API 核心语法理解(6)

Vue3 取消了 this 取而代之的是 setup 增加了 2 个参数:

  • props,组件参数
  • context,上下文信息
setup(props, context) {
// props
// context.attrs
// context.slots
// context.emit
}

基本使用方法如下

export default {
components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList },
props: {
user: { type: String }
},
setup(props) {
console.log(props) // { user: '' }

return {} // 这里返回的任何内容都可以用于组件的其余部分
}
// 组件的“其余部分”
}

setup 是 Composition API 的入口函数,可以说也是整个 Vue3.x 的核心;setup 返回的所有内容都将暴露给组件的其余部分 (计算属性、方法、生命周期钩子等等) 以及组件的模板。

监听变量的变化:需要使用 ref 来创建变量

注意:ref 只能监听简单类型的数据变化,不能监听复杂类型的变化(对象、数组),复杂类型的变量需要使用 reactive

下面使用一个添加 TODO 的例子来说明使用这个组织方式的好处

模块化:添加 TODO 的例子

创建一个 useRemoveTodo.js 模块专门用于处理移除 TODO 的操作

import { reactive } from 'vue' 
// 别忘记了这里也依赖了 vue 的 reactive 模块

// 抽离出来
function useRemoveTodo() {
// 要监听复杂类型需要使用 reactive
const object = reactive({
todies: [
{ id: 1, value: '7 点起床' },
{ id: 2, value: '8 点上课' },
{ id: 3, value: '11 点吃饭' },
{ id: 4, value: '12 点睡觉' }
]
})

function remove(index) {
object.todies = object.todies.filter((value, idx) => idx !== index)
}

return {
object,
remove
}
}

export default useRemoveTodo;

创建一个 useAddTodo.js 模块专门用于添加 TODO 的操作

import { reactive } from 'vue'
// 别忘记了这里也依赖了 vue 的 reactive 模块

// 注意因为修改了 object 这个对象里的 todies 所以需要把 object 传入进来
function useAddTodo(object) {
// 存储用于添加的 Todo
const todo = reactive({
id: 0,
value: ''
})

// 添加 Todo
function addTodo(e) {
// 取消事件的默认动作(这里的提交刷新)
e.preventDefault()
console.log(todo) // Proxy {id: "5", value: "14 点上课"}

// 这个就不解释了,看 JavaScript学习02 那一篇的 Object 对象拷贝
const temp = Object.assign({}, todo)
object.todies.push(temp)
}

return {
todo,
addTodo
}
}

export default useAddTodo

然后就可以直接在组件处引入这两个定义好的模块,使文件变得简洁

<template>
<div>{{ count }}</div>
<button @click="add()">点击 count +1</button>
<br />
<div>
<ul v-for="(item, index) in object.todies" :key="index">
<li @click="remove(index)">{{ item.id }} ==== {{ item.value }}</li>
</ul>
</div>
<br />
<form>
<input type="number" v-model="todo.id" />
<input type="text" v-model="todo.value" />
<input type="submit" @click="addTodo" />
</form>
</template>

<script>
import { ref, reactive } from 'vue'
// 引入自定义的模块
import useRemoveTodo from './module/useRemoveTodo.js'
import useAddTodo from './module/useAddTodo.js'

export default {
setup() {
// 需要监听一个变量变化需要使用 ref 创建
// 这里创建了一个叫做 count 的变量,且其初始值为 0
// 注意:ref 只能监听简单类型的数据变化,不能监听复杂类型的变化(对象、数组)
const count = ref(0)

function add() {
// 注意不能直接用 count++
count.value++
}

// 把各个模块抽离到其它文件去可以导致组件这块保持整洁
const { object, remove } = useRemoveTodo()
const { todo, addTodo } = useAddTodo(object)

// 在组合 API 中定义的变量/方法,想要在外界使用需要使用 return 暴露出去
return {
remove,
count,
object,
add,
addTodo,
todo
}
}
}
</script>
<style scoped>
li {
background-color: blue;
}
</style>

组件的脚本部分的不同写法

参考文档 Vue 装饰器写法 写法参考 Vue Class Component

包装成类的写法

这种包装成类的写法 Vue3 之前就有了,且官方已经在逐步放弃了(详情看:[Abandoned] Class API proposal

可以看到这种写法可以不写 setup 直接写方法就能调用

<template>
<div>
<button v-on:click="decrement">-</button>
{{ count }}
<button v-on:click="increment">+</button>
</div>
</template>

<script>
import Vue from 'vue'
import Component from 'vue-class-component'

// Define the component in class-style
@Component
export default class Counter extends Vue {
// Class properties will be component data
count = 0

// Methods will be component methods
increment() {
this.count++
}

decrement() {
this.count--
}
}
</script>

使用 defineComponent

参考资料 defineComponent

Vue3 TS 的原生写法,我觉得还是转成这种写法好点

import { defineComponent, reactive } from 'vue';
import Parent from '@/components/Parent.vue';

export default defineComponent({
components: {Parent},
setup(){
return reactive({
message: "Hello, Vue3.0 with Typescript"
})
}
})

这个 defineComponent 在 TypeScript 下,给予了组件正确的参数类型推断(就是方便直接 import 其它的 vue 文件)

响应式工具

reactive 包装对象

参考资料 响应性基础 API

reactive 是 Vue3 中提供的实现响应式数据的工具,它能把对象包装成 Proxy 对象,使之用于响应式的特性

  • 在 Vue2 中的响应式数据是通过 defineProperty 来实现的
  • 而在 Vue3 中响应式数据则是通过 ES6 的 Proxy 来实现的

reactive 参数必须是 对象,如果是普通的参数 reactive 无法将其包装成 Proxy 对象,响应式特性就将消失

interface LoginState {
username: string;
password: string;
code: string;
remember: boolean;
}

const state = reactive({
username: "",
password: "",
code: "",
remember: false
}) as LoginState;

ref 包装基本类型数据

ref 底层本质还是一个 reactive,系统会自动根据我们给 ref 传入的值将它转换成 ref(xx) --> reactive({ value: xx})

所以在 JS 里取得 ref 的值必须通过 value 获取(在模板中使用不用加 value

const count = ref(0)
console.log(count.value) // 0

count.value++
console.log(count.value) // 1

如果使用 TS 需要类型声明:

// 这个 ref 方法返回的类型是一个 Ref 类型
function ref<T>(value: T): Ref<T>

// 这个 Ref 类型的内部
interface Ref<T> {
value: T
}


// 使用例子
const foo = ref<string | number>('foo') // foo's type: Ref<string | number>
foo.value = 123 // ok!

如果泛型的类型未知,则应该使用 ref 转换为 Ref<T>

function useState<State extends string>(initial: State) {
const state = ref(initial) as Ref<State> // state.value -> State extends string
return state
}

如果想要判断参数为 ref,则返回内部值,否则返回参数本身

function useFoo(x: number | Ref<number>) {
// 相当于 val = isRef(val) ? val.value : val
const unwrapped = unref(x) // unwrapped 确保现在是数字类型
}

isRef 的使用,isRef() 函数主要用来判断某个值是否为 ref() 创建出来的对象;

ref 获取标签元素

在 Vue2 中,获取元素都是通过给元素一个 ref 属性,然后通过 this.$refs.xx 来访问的,但这在 Vue3 中已经不再适用了

Vue3 使用 ref 方法创建一个引用

<template>
<div>
<div ref="el">div元素</div>
</div>
</template>

<script>
import { ref, onMounted } from 'vue'
export default {
setup() {
// 创建一个DOM引用,名称必须与元素的ref属性名相同
const el = ref(null)

// 在挂载后才能通过 el 获取到目标元素
onMounted(() => {
el.value.innerHTML = '内容被修改'
})

// 把创建的引用 return 出去
return {el}
}
}
</script>

这里举个 Element-Plus 的例子

看文档 Element-plus 还是大量的使用这种 this.$refs 的写法写法

this.$refs[formName].resetFields();

但是在 Vue3 已经不支持这种写法了,所以得了解下这个 $refs 是什么?

<div id="app">
<input type="text" ref="input1"/>
<button @click="add">添加</button>
</div>
new Vue({
el: "#app",
methods:{
add:function(){
this.$refs.input1.value ="22"; //this.$refs.input1 减少获取dom节点的消耗
}
}
})

参考资料 Vue.js の Composition API における this.$refs の取得方法 如上,单纯就是用来取得这个 DOM 或者一个封装的节点,所以只需取得 Element 相应的组件就行了

import { ElForm } from "element-plus";
// ...

// InstanceType 的作用是获取构造函数类型的实例类型
// typeof 可以用于从一个变量上获取它的类型。
// type Instance = InstanceType<typeof TestClass>; // TestClass 固定用法
const formRef = ref<InstanceType<typeof ElForm>>();

// ...
formRef.value?.validate(async valid => {
if (!valid) {
return message.error("请把信息填写完整!");
}
// ...
});
// 最后要暴露这个 ref
return { formRef };

然后再在这个组件上使用 ref 引用

<el-form label-width="70px" :rules="rules" :model="state" ref="formRef">

toRef 引用对象部分属性

参考资料 快速使用Vue3最新的15个常用API 参考资料 Vue3.0尝试

使用 toRef 可以将某个使用了 reactive 的响应对象中的某个属性单独拿出来,但这只是引用关系,所以修改这个引用的数据是会影响到原始数据的。

toRef 接收两个参数,第一个参数是哪个对象,第二个参数是对象的哪个属性

const state = reactive({
foo: 1,
bar: 2
})

const fooRef = toRef(state, 'foo')

fooRef.value++
console.log(state.foo) // 2

state.foo++
console.log(fooRef.value) // 3

toRefs 将对象的属性变成 ref

toRefs() 函数可以将 reactive() 创建出来的响应式对象,转为普通对象,只不过这个对象上的每个属性都是以 ref() 工具创建出来的。

好处就是 <template> 里不用像对象一样先把属性 . 出来才能使用,使用场景如下

import { toRefs, reactive } from '@vue/composition-api'

setup() {
// 定义响应式数据对象
const state = reactive({
count: 0
})

// 定义页面上可用的事件处理函数
const increment = () => {
state.count++
}

// 在 setup 中返回一个对象供页面使用
// 这个对象中可以包含响应式的数据,也可以包含事件处理函数
return {
// 将 state 上的每个属性,都转化为 ref 形式的响应式数据(这个 ... 是 TS 的展开运算符)
...toRefs(state),
// 自增的事件处理函数
increment
}
}

在 template 中就直接可以使用 count 属性和相对应的 increment 方法了,如果没有使用 toRefs 直接返回 state 那么就得通过 state.xx 来访问数据

<template>
<div>
<span>当前的count值为:{{count}}</span>
<button @click="increment">add</button>
</div>
</template>

补充一下这个展开运算符

let list = [1, 2];
list = [...list, 3, 4];
console.log(list); // [1,2,3,4]

toRaw 直接操作原始值

toRaw 方法是用于直接获取 ref 或 reactive 对象的原始数据的引用,而不是 Proxy,当需要变更数据但是不想触发响应的时候就可以用它

<template>
<p>{{ state.name }}</p>
<p>{{ state.age }}</p>
<button @click="change">改变</button>
</template>

<script>
import {reactive} from 'vue'
export default {
setup() {
const obj = {
name: '前端印象',
age: 22
}

const state = reactive(obj)

function change() {
state.age = 90
console.log(obj); // 打印原始数据obj
console.log(state); // 打印 reactive对象
}

return {state, change}
}
}
</script>

如果这时更新原始数据 obj 的值,那 reactive 的值也会跟着改变,但是视图不更新。由此可见,当我们想修改数据,但不想让视图更新时,可以选择直接修改原始数据上的值,因此需要先获取到原始数据,我们可以使用 Vue3 提供的 toRaw 方法

<script>
import {reactive, toRaw} from 'vue'
export default {
setup() {
const obj = {
name: 'temp',
age: 22
}

const state = reactive(obj)
const raw = toRaw(state)

console.log(obj === raw) // true
}
}
</script>

注意: 当 toRaw 方法接收的参数是 ref 对象时,需要加上 .value 才能获取到原始数据对象

getCurrentInstance 使用全局对象

如何使用全局变量呢?在 Vue3 已经改变了 this 的指向,所以像 Element-plus 这种强依赖全局对象的工具如何取得绑定在全局对象上的变量呢?这时就可以使用 getCurrentInstance 取得当前实例

直接使用全局(注意加个 ? 安全链式调用)

import { defineComponent, reactive, getCurrentInstance } from "vue";

const message = getCurrentInstance()?.appContext.config.globalProperties.$message;
message({
showClose: true,
message: "错了哦,这是一条错误消息",
type: "error"
});

useStore 取得 Vuex 实例

在 Vue2 中使用 Vuex,都是通过 this.$store 来与获取到 Vuex 实例

在 Vue3 需要使用这个 useStore 方法

// store 文件夹下的 index.js
import Vuex from 'vuex'

const store = Vuex.createStore({
state: {
name: '前端印象',
age: 22
},
mutations: {
…… // 别忘了需要通过 mutations 才能变更 state 里面的值
},
……
})
// example.vue
// 从 vuex 中导入 useStore 方法
import {useStore} from 'vuex'
export default {
setup() {
// 获取 vuex 实例
const store = useStore()

console.log(store)
}
}

props 和 context

参考资料 组合式 API

props: {
title: String
},
setup(props, context) {
console.log(props.title)

// Attribute (非响应式对象)
console.log(context.attrs)

// 插槽 (非响应式对象)
console.log(context.slots)

// 触发事件 (方法)
console.log(context.emit)
context.emit('title-changed') // 就是之前的发射一个事件
}

props 插槽

当父子组件需要传递信息时就可以通过这个插槽

export default {
props: {
name: String
},
setup(props) {
console.log(props.name)
}
}

此 props 对象是响应式的——即在传入新的 props 时会对其进行更新,并且可以通过使用 watchEffect 或 watch 进行观测和响应:

export default {
props: {
name: String
},
setup(props) {
watchEffect(() => {
console.log(`name is: ` + props.name)
})
}
}

context 上下文

该对象暴露了以前在 this 上暴露的 property 的选择列表:

const MyComponent = {
setup(props, context) {
context.attrs
context.slots
context.emit
}
}

Vue3 使用 watch

<template>
<button @click="change">count is: {{ state.count }}</button>
</template>

<script>
import { reactive, watch } from 'vue'
export default {
setup () {
let state = reactive({count: 0})
let change = () => state.count++;

watch(state, () => {
console.log(state, '改变')
})
return { state, change }
}
}
</script>

在组件中监听 vuex 数据变化

//利用计算属性
computed: {
listData() {
return this.$store.state.listData;
}
},
//监听执行
watch: {
listData(val) {
写上你需要的东西
}
},